package ar.fi.uba.fallas2.business;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import com.sun.org.apache.xerces.internal.impl.dv.DatatypeException;

import sun.reflect.generics.factory.GenericsFactory;

import ar.fi.uba.fallas2.RealQAM;
import ar.fi.uba.fallas2.data.DataException;
import ar.fi.uba.fallas2.data.Database;
import ar.fi.uba.fallas2.helpers.BundleReader;

public class KnowledgeMatrix {
	private HashMap<Question, HashMap<GuessObject, KindWeightPair>> matrixByQuestion;
	private HashMap<GuessObject, HashMap<Question, KindWeightPair>> matrixByObject;
	private ArrayList<GuessObject> missedObjects;
	private HashMap<Question, AnswerKind> actualAnswers;
	private double lastBestSimilarity;
	
	// TODO: ver si es un valor fijo, o si es variable segun las respuestas y los matches previos
	private static final double OBJECT_SIMILARITY_TOLERANCE 		= Double.parseDouble(BundleReader.getProperty("knowledge.objectSimilarityTolerance"));
	private static final double MIN_OBJECT_SIMILARITY_FOR_GUESSING 	= Double.parseDouble(BundleReader.getProperty("knowledge.minObjectSimilarityForGuessing"));
	
	public KnowledgeMatrix() {
		this.setMatrixByQuestion(new HashMap<Question, HashMap<GuessObject, KindWeightPair>>());
		this.setMatrixByObject(new HashMap<GuessObject, HashMap<Question,KindWeightPair>>());
		this.setMissedObjects(new ArrayList<GuessObject>());
		this.setActualAnswers(new HashMap<Question, AnswerKind>());
		this.setLastBestSimilarity(0);
		this.init();
	}
	
	private void init() {
		try {
			List<Question> questions = GenericFinder.findAllQuestions();
			for(Question q : questions) {
				this.getMatrixByQuestion().put(q, new HashMap<GuessObject, KindWeightPair>());
			}
			
			List<Answer> answers = GenericFinder.findAllAnswers();
			for(Answer a : answers) {
				Question q 		= a.getQuestion();
				AnswerKind k 	= a.getKind();
				GuessObject o 	= a.getObject();
				KindWeightPair p= new KindWeightPair(k, a.getWeight());
				
				if (!this.getMatrixByQuestion().containsKey(q)) 
					this.getMatrixByQuestion().put(q, new HashMap<GuessObject, KindWeightPair>());
				this.getMatrixByQuestion().get(q).put(o, p);
				
				if (!this.getMatrixByObject().containsKey(o)) 
					this.getMatrixByObject().put(o, new HashMap<Question, KindWeightPair>());
				this.getMatrixByObject().get(o).put(q, p);
			}
		} catch (DataException ex) {
			ex.printStackTrace();
		}
	}
	
	public void update(GuessObject obj) 
	{
		// Si la matriz ya contiene al objeto, entonces se actualizan sus pesos. De lo
		// contrario, se crea la columna.
		if (this.getMatrixByObject().containsKey(obj)) {
			HashMap<Question, KindWeightPair> questions = this.getMatrixByObject().get(obj);
			for (Question q : questions.keySet()) {
				AnswerKind newAnswer 		= this.getActualAnswers().get(q);
				if (newAnswer != null) {
					KindWeightPair lastAnswer 	= questions.get(q);
					lastAnswer.updateWeight(newAnswer);
				}
			}
		} else {
			// Se escribe toda la columna de preguntas (y respuestas) llenando con ceros
			// cuando no se uso a dicha pregunta y con el valor que corresponda cuando si.
			HashMap<Question, KindWeightPair> tmpQuestionsWithWeight = new HashMap<Question, KindWeightPair>();
			
			
			for (Question q : this.getActualAnswers().keySet()) {
				AnswerKind newKind = this.getActualAnswers().get(q);
				int weight = KindWeightPair.UNKNOWN_WEIGHT;
				KindWeightPair pair = null;
				
				// Si existe, entonces se setea con el valor inicial que le corresponda. De lo contrario, con UNKNOWN.
				if (newKind != null) {
					weight 	= (newKind.equals(AnswerKind.YES) || newKind.equals(AnswerKind.NO)) ? KindWeightPair.INIT_YES_NO_WEIGHT : KindWeightPair.INIT_PROBABLY_WEIGHT;
					pair 	= new KindWeightPair(newKind, weight);
				} else {
					// Por defecto, se guarda con UNKNOWN y con tipo NO.
					// TODO: ver
					// NUNCA ENTRA ACA, YA QEU SE FILTRAN ANTES
					// pair = new KindWeightPair(AnswerKind.NO, weight);
				}
				
				tmpQuestionsWithWeight.put(q, pair);
				
				if (!this.getMatrixByQuestion().containsKey(q)) this.getMatrixByQuestion().put(q, new HashMap<GuessObject, KindWeightPair>());
				this.getMatrixByQuestion().get(q).put(obj, pair);
			}
			
			this.getMatrixByObject().put(obj, tmpQuestionsWithWeight);
		}
		
		this.saveToDB();
	}
	
	private void saveToDB() {
		try {
			// Primero se eliminan TODAS las respuestas previas.
			Answer.deleteAll();

			// Para luego, insertar TODAS las respuestas existentes (las anteriores + las actualizadas)
			for (Question q : this.getAllQuestions()) {
				for (GuessObject o : this.getAllObjects()) {
					KindWeightPair p = this.getMatrixByQuestion().get(q).get(o);
					
					// Si se contesto la pregunta q para el objeto o
					if (p != null) {
						Answer a = new Answer();
						a.setQuestion(q);
						a.setObject(o);
						a.setKind(p.getKind());
						a.setWeight(p.getWeight());
						a.save();
					}
				}
			}
	
			Database.getCloseInstance();
		} catch (DataException ex) {
			ex.printStackTrace();
		}
	}
	
	private GuessObject[] getAllObjects() {
		GuessObject[] objs = new GuessObject[this.getMatrixByObject().keySet().size()];
		return this.getMatrixByObject().keySet().toArray(objs);
	}
	
	public GuessObject getObjectByName(String objectName) {
		for(GuessObject o : this.getAllObjects()) {
			if (o.getName().equals(objectName)) return o;
		}
		
		try {
			return GenericFinder.findObjectByName(objectName);
		} catch (DataException ex) {
			return null;
		}
	}
	
	private Question[] getAllQuestions() {
		Question[] qs = new Question[this.getMatrixByQuestion().keySet().size()];
		return this.getMatrixByQuestion().keySet().toArray(qs);
	}
	
	public void addMissedObject(GuessObject obj) {
		this.getMissedObjects().add(obj);
	}
	
	public void addActualAnswer(Question q, AnswerKind k) {
		this.getActualAnswers().put(q, k);
	}
	
	public Question getNextQuestion() {
		ArrayList<GuessObject> probableObjects = this.getProbableObjects();
		
		// Si todavia no se han hecho preguntas
		if (probableObjects.size() == 0) {
			return this.getRandomQuestion();
		} else {
			HashMap<Question, ArrayList<GuessObject>> yesNoQuestions = this.getYesNoQuestions(probableObjects);
			Question nextQuestion = this.getQuestionByRatio(yesNoQuestions);
			// Si no hay mas preguntas para hacer, entonces se elige una random
			return nextQuestion != null ? nextQuestion : this.getRandomQuestion();
		}
	}
	
	private Question getRandomQuestion() {
		for (Question q : this.getAllQuestions()) {
			if (!this.getActualAnswers().containsKey(q)) return q;
		}
		return null;
	}
	
	// TODO: ver de refactorizar, es MUY parecido al getProbableObjects
	public GuessObject getNextProbableObject() {
		GuessObject nextObject = null;
		int totalAnswers = this.getActualAnswers().keySet().size();
		//int totalQuestions = this.getAllQuestions().length;
		double bestSimilarity = 0;
		
		// Se compara cada uno de los objetos existentes (excepto los missed), con el que se quiere adivinar
		for (GuessObject obj : this.getAllObjects()) {
			if (!this.getMissedObjects().contains(obj)) {
				int matchAnswers 		= 0;
				int almostMatchAnswer 	= 0;
				int noMatchAnswers 		= 0;
				int inexistentAnswers 	= 0;
				double thisSimilarity 	= 0;
				int totalQuestions		= this.getMatrixByObject().get(obj).size();
				
				// Se recorren todas las preguntas para cada objeto, y se las compara con 
				// las que se respondieron del objeto a adivinar
				for (Question q : this.getAllQuestions()) {
					if (this.getActualAnswers().containsKey(q)) {
						KindWeightPair pair = this.getMatrixByObject().get(obj).get(q);
						AnswerKind guessingKind = this.getActualAnswers().get(q);
						
						// Si pair es null, quiere decir que la pregunta q para el objeto obj no ha sido contestada aun
						if (pair == null) inexistentAnswers++; 
						
						else if (guessingKind.equals(pair.getKind())) matchAnswers++;
						
						// Si son de la misma familia, es decir: YES con PROBABLY, o NO con DOUBTFUL (es decir, 
						// no se de el caso en que una es YES y la otra es NO)
						else if ((pair.getKind().equals(AnswerKind.YES) && guessingKind.equals(AnswerKind.NO)) || 
								 (pair.getKind().equals(AnswerKind.NO) && guessingKind.equals(AnswerKind.YES))) almostMatchAnswer++;
						
						else noMatchAnswers++;
					}
				}
				
				thisSimilarity = this.calculateObjectSimilarity(totalQuestions, totalAnswers, matchAnswers, almostMatchAnswer, noMatchAnswers, inexistentAnswers);
				
				if (thisSimilarity > bestSimilarity) {
					bestSimilarity = thisSimilarity;
					nextObject = obj;
				}
			}
		}
		this.setLastBestSimilarity(bestSimilarity);
		
		return nextObject;
	}
	
	public boolean canGuessObject() {
		return this.getLastBestSimilarity() >= MIN_OBJECT_SIMILARITY_FOR_GUESSING;
	}
	
	public ArrayList<GuessObject> getProbableObjects() {
		ArrayList<GuessObject> probableObjects = new ArrayList<GuessObject>();
		int totalAnswers = this.getActualAnswers().keySet().size();
		//int totalQuestions = this.getAllQuestions().length;
		
		// Se compara cada uno de los objetos existentes (excepto los missed), con el que se quiere adivinar
		for (GuessObject obj : this.getAllObjects()) {
			if (!this.getMissedObjects().contains(obj)) {
				int matchAnswers 		= 0;
				int almostMatchAnswer 	= 0;
				int noMatchAnswers 		= 0;
				int inexistentAnswers 	= 0;
				int totalQuestions		= this.getMatrixByObject().get(obj).size();
				
				// Se recorren todas las preguntas para cada objeto, y se las compara con 
				// las que se respondieron del objeto a adivinar
				for (Question q : this.getAllQuestions()) {
					if (this.getActualAnswers().containsKey(q)) {
						KindWeightPair pair 	= this.getMatrixByObject().get(obj).get(q);
						AnswerKind guessingKind = this.getActualAnswers().get(q);
						
						// Si pair es null, quiere decir que la pregunta q para el objeto obj no ha sido contestada aun
						if (pair == null) inexistentAnswers++; 
						
						else if (guessingKind.equals(pair.getKind())) matchAnswers++;
						
						// Si son de la misma familia, es decir: YES con PROBABLY, o NO con DOUBTFUL (es decir, 
						// no se de el caso en que una es YES y la otra es NO)
						else if ((pair.getKind().equals(AnswerKind.YES) && guessingKind.equals(AnswerKind.NO)) || 
								 (pair.getKind().equals(AnswerKind.NO) && guessingKind.equals(AnswerKind.YES))) almostMatchAnswer++;
						
						else noMatchAnswers++;
					}
				}
				
				if (this.calculateObjectSimilarity(totalQuestions, totalAnswers, matchAnswers, almostMatchAnswer, noMatchAnswers, inexistentAnswers) 
					>= OBJECT_SIMILARITY_TOLERANCE) {
					probableObjects.add(obj);
				}
				System.out.println(totalAnswers + ": " +obj.getName() + " - " +  this.calculateObjectSimilarity(totalQuestions, totalAnswers, matchAnswers, almostMatchAnswer, noMatchAnswers, inexistentAnswers));
			}
		}
		
		return probableObjects;
	}
	
	private double calculateObjectSimilarity(int total, int totalAnswers, int match, int almostMatch, int noMatch, int inexistentAnswers) {
		// La formula para calcular similitud entre objectos, toma como valor entero a cada match, y le da
		// 1/4 a los "casi" matches.
		// return (match + (almostMatch * 0.25)) / totalAnswers;
		double dtotal = RealQAM.MAX_QUESTIONS;
		double dtotalAnswers=totalAnswers ; 
		double dmatch=match;
		double dalmostMatch=almostMatch;
		
		double ponderacion=0.45;
		double ponderacionAlmost=0.25;
		try {
			double val1= dmatch/dtotalAnswers*ponderacion + dalmostMatch/dtotalAnswers*ponderacion*ponderacionAlmost;
			double val2= dmatch/dtotal*(1-ponderacion) + dalmostMatch/dtotal*(1-ponderacion)*ponderacionAlmost;
			//System.out.println("CALCULO: match:" + dmatch + ", totalAns:" + dtotalAnswers + " dtotal:" + dtotal);
			//System.out.println("RESULTADO CALC: =" + val1+" + " + val2 + " = " + (val1+val2));
			return val1+val2;
		} catch (Exception e) {
			return 0;
		}
	}
	
	private HashMap<Question, ArrayList<GuessObject>> getYesNoQuestions(ArrayList<GuessObject> probableObjects) {
		HashMap<Question, ArrayList<GuessObject>> yesNoQuestions = new HashMap<Question, ArrayList<GuessObject>>();
		
		for(GuessObject obj : probableObjects) {
			for (Question q : this.getAllQuestions()) {
				// Si la pregunta no fue hecha aun..
				if (!this.getActualAnswers().containsKey(q)) {
					KindWeightPair pair = this.getMatrixByObject().get(obj).get(q);
					
					// Si la base de concocimiento tiene informacion acerca de la pregunta q para el objeto obj.
					if (pair != null) {
						if (pair.getKind().equals(AnswerKind.YES) || pair.equals(AnswerKind.NO)) {
							if (!yesNoQuestions.containsKey(q)) yesNoQuestions.put(q, new ArrayList<GuessObject>());
							yesNoQuestions.get(q).add(obj);
						}
					}
				}
			}
		}
		
		return yesNoQuestions;
	}
	
	private Question getQuestionByRatio(HashMap<Question, ArrayList<GuessObject>> yesNoQuestions) {
		double bestRatio = 0;
		Question chosenQuestion = null;
		
		// Se elije la pregunta con mejor ratio de YES
		for (Question q : yesNoQuestions.keySet()) {
			double ratio = this.getQuestionRatio(q, yesNoQuestions.get(q));
			if (ratio > bestRatio) chosenQuestion = q;
		}
		
		return chosenQuestion;
	}
	
	private double getQuestionRatio(Question q, ArrayList<GuessObject> objs) {
		int total = objs.size();
		int yes = 0;
		
		for(GuessObject obj : objs) {
			KindWeightPair pair = this.getMatrixByQuestion().get(q).get(obj);
			
			// Como las preguntas que llegaron hasta aca, son todas YES/NO, simplemente
			// se toman en cuanta las YES (las restantes seran NO)
			if (pair.getKind().equals(AnswerKind.YES)) yes++;
		}
		
		return yes / total;
	}
	
	// Setter's y Getter's

	private void setMatrixByQuestion(HashMap<Question, HashMap<GuessObject, KindWeightPair>> matrixByQuestion) {
		this.matrixByQuestion = matrixByQuestion;
	}

	private HashMap<Question, HashMap<GuessObject, KindWeightPair>> getMatrixByQuestion() {
		return matrixByQuestion;
	}

	private void setMatrixByObject(HashMap<GuessObject, HashMap<Question, KindWeightPair>> matrixByObject) {
		this.matrixByObject = matrixByObject;
	}

	private HashMap<GuessObject, HashMap<Question, KindWeightPair>> getMatrixByObject() {
		return matrixByObject;
	}

	private void setMissedObjects(ArrayList<GuessObject> missedObjects) {
		this.missedObjects = missedObjects;
	}

	private ArrayList<GuessObject> getMissedObjects() {
		return missedObjects;
	}

	private void setActualAnswers(HashMap<Question, AnswerKind> actualAnswers) {
		this.actualAnswers = actualAnswers;
	}

	private HashMap<Question, AnswerKind> getActualAnswers() {
		return actualAnswers;
	}

	private void setLastBestSimilarity(double lastBestSimilarity) {
		this.lastBestSimilarity = lastBestSimilarity;
	}

	private double getLastBestSimilarity() {
		return lastBestSimilarity;
	}

}